/**
 * Copyright (c) 2010-2011 Pierre LABATUT
 *
 * This file is part of multiple-track-player project.
 *
 * multiple-track-player is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or <http://code.google.com/p/multiple-track-player/>.
 */
package multitrack.preload;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingWorker;

/**
 * Swing panel component that shows a waveform, a play cursor position and a mouse 
 * cursor position.
 * 
 * @author Pierre LABATUT
 */
public class HeapAudioBufferView extends javax.swing.JPanel {

    public static final int REPAINT_DELAY = 200;
    private final AudioPlayerView apv;
    private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "Draw Waveform");
        }
    });
    int playPositionInFrame = 0;
    //-1 means mous not in the area
    int mousePositionInPixel = -1;
    private final MixAudioBuffer mix;

    /** Creates new form NewJPanel */
    public HeapAudioBufferView() {
        mix = null;
        apv = null;
        buffer = null;
        initComponents();
    }

    public HeapAudioBufferView(final AudioPlayerView apv, final MixAudioBuffer mix, final HeapAudioBuffer buffer) {
        if (apv == null) {
            throw new IllegalArgumentException();
        }
        this.apv = apv;
        if (buffer == null) {
            throw new IllegalArgumentException();
        }
        this.mix = mix;
        if (mix == null) {
            throw new IllegalArgumentException();
        }
        this.buffer = buffer;
        initComponents();
        addMouseMotionListener(new MouseWatch());
        addMouseListener(new MouseWatch());

        addComponentListener(new ComponentListener() {

            @Override
            public void componentResized(ComponentEvent e) {
                if (getWidth() > 0 && getHeight() > 0) {
                    if (task != null && !task.isDone() && !task.isCancelled()) {
                        task.cancel(true);
                    }
                    task = new RenderWaveform(getWidth(), getHeight());
                    executor.schedule(task, REPAINT_DELAY, TimeUnit.MILLISECONDS);
                }
            }

            @Override
            public void componentMoved(ComponentEvent e) {
            }

            @Override
            public void componentShown(ComponentEvent e) {
                if (getWidth() > 0 && getHeight() > 0) {
                    if (task == null || task.isCancelled()) {
                        task = new RenderWaveform(getWidth(), getHeight());
                        executor.schedule(task, REPAINT_DELAY, TimeUnit.MILLISECONDS);
                    }
                }
            }

            @Override
            public void componentHidden(ComponentEvent e) {

                if (task != null && !task.isDone() && !task.isCancelled()) {
                    task.cancel(true);
                }
                task = null;
            }
        });
    }

    void doSeek(int framePosition) {

        apv.doSeek(framePosition);
    }

    void updatePlayCursor(int framePosition) {
        this.playPositionInFrame = framePosition;
    }
    private final Color waveBorder = new Color(110, 110, 110);
    private final Color waveCenter = new Color(REPAINT_DELAY, REPAINT_DELAY, REPAINT_DELAY);
    private final Color background = new Color(230, 230, 230);

    class RenderWaveform extends SwingWorker<WaveForm, WaveForm> {

        final int width;
        final int height;

        public RenderWaveform(int width, int height) {
            if (width <= 0) {
                throw new IllegalArgumentException("Width must be strictly positive");
            }
            if (height <= 0) {
                throw new IllegalArgumentException("Height must be strictly positive");
            }
            this.width = width;
            this.height = height;

        }

        @Override
        protected WaveForm doInBackground() throws Exception {

            do {
                // Build the result output structure
                final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
                final int channels = buffer.format().getChannels();
                final int totalSample = buffer.samples();
                final int totalFrames = totalSample / channels;
                final WaveForm wf = new WaveForm(image, totalFrames);

                // Clear the image with a grey
                final Graphics2D graphics = (Graphics2D) image.getGraphics();
                graphics.setBackground(background);
                graphics.clearRect(0, 0, width, height);
                // Setup a gradient paint tool to draw a cute waveform
                graphics.setPaint(new GradientPaint(0, height / 16, waveCenter, 0, height / 2, waveBorder, true));
                float halfHeight = height * .5f;

                float pixelToFrame = (totalFrames + 1.f) / width;
                boolean notYetBlocked = true;

                // Get the current available sample count. All this data is avaialble in memory.
                // I don't want to publish intermediate result until I reached this sample index.
                // As a consequence when the buffer is completed (everything is in memory) there 
                // is no intermediate result at all;           
                int available = buffer.samples();
                short minmax[] = {0, 0};

                for (int x = 0; x < width; x++) {
                    // I'm going to render a pixel column of the waveform, this pixel 
                    // column stands for the following range of sample                
                    int fromSample = ((int) (x * pixelToFrame)) * channels;
                    int toSample = ((int) ((x + 1.f) * pixelToFrame)) * channels;

                    if (notYetBlocked) {
                        // I did not reached 'available' yet
                        if (toSample > available) {
                            publish(wf);
                            notYetBlocked = false;
                        }
                    } else {
                        // I reached the available limit, now publish every
                        publish(wf);
                    }
                    minmax[0] = minmax[1] = 0;
                    buffer.minmax(fromSample, toSample, minmax);
                    float max = (float) minmax[0] / Short.MAX_VALUE;
                    float min = (float) minmax[1] / Short.MAX_VALUE;
                    graphics.drawLine(x, (int) (halfHeight - max * halfHeight),
                            x, (int) (halfHeight - min * halfHeight));
                }

                if (buffer.isCompleteWith(totalSample)) {
                    return wf;
                }
                publish(wf);
                long reference = System.currentTimeMillis();
                //Frames for one more pixel :)
                final double expectedFrame = totalSample * (width + 8.) / width + 1.;
                buffer.waitAvailableOrComplete((int) expectedFrame);
                long now = System.currentTimeMillis();
                if (now - reference < REPAINT_DELAY) {
                    Thread.sleep(REPAINT_DELAY - reference + now);
                }
            } while (true);

        }

        @Override
        protected void process(List<WaveForm> chunks) {
            HeapAudioBufferView.this.image = chunks.get(chunks.size() - 1);

            apv.repaint();
        }

        @Override
        protected void done() {
            if (isCancelled()) {
                return;
            }
            try {
                HeapAudioBufferView.this.image = get();
                apv.repaint();
            } catch (InterruptedException ex) {
                Logger.getLogger(HeapAudioBufferView.class.getName()).log(Level.SEVERE, null, ex);
            } catch (ExecutionException ex) {
                Logger.getLogger(HeapAudioBufferView.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    };
    final HeapAudioBuffer buffer;
    RenderWaveform task;

    class WaveForm {

        public WaveForm(BufferedImage image, int frames) {
            this.frames = frames;
            this.image = image;
        }
        final int frames;
        final BufferedImage image;
    }
    WaveForm image;

    class MouseWatch extends MouseAdapter {

        @Override
        public void mouseReleased(MouseEvent e) {
            if (image != null) {
                doSeek(pixelToFrame(e.getX()));
            }
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            mousePositionInPixel = e.getX();
        }

        @Override
        public void mouseClicked(MouseEvent e) {
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            mousePositionInPixel = e.getX();
        }

        @Override
        public void mouseExited(MouseEvent e) {
            mousePositionInPixel = -1;
        }
    }

    private int pixelToFrame(int xPos) {
        return (int) ((double) xPos * image.frames / getWidth());

    }

    @Override
    public void paintComponent(Graphics g) {

        final Graphics2D graphics = (Graphics2D) g;
        double frameToPixel = (double) getWidth() * mix.format().getChannels() / mix.samples();

        if (image != null) {
            //Draw an image an streach if necesary

            graphics.drawImage(image.image, 0, 0, (int) (image.frames * frameToPixel), getHeight(), null);
        } else {
            //No image draw the background of the image and start a computing thread
            graphics.setBackground(background);
            graphics.clearRect(0, 0, getWidth(), getHeight());
            if (getWidth() > 0 && getHeight() > 0) {
                if (task == null || task.isCancelled()) {
                    task = new RenderWaveform(getWidth(), getHeight());
                    executor.schedule(task, REPAINT_DELAY, TimeUnit.MILLISECONDS);
                }
            }
        }
        // Draw the mouse line cursor
        graphics.setColor(Color.ORANGE);
        g.drawLine(mousePositionInPixel, 0, mousePositionInPixel, getHeight());

        // Draw the play cursor
        if (image != null) {
            final int playPositionInPixel = (int) (playPositionInFrame * frameToPixel);
            graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .30f));
            graphics.setPaint(new GradientPaint(playPositionInPixel - 20, 0, Color.ORANGE, playPositionInPixel, 0, Color.YELLOW));
            g.fillRect(0, 0, playPositionInPixel, getHeight());
            graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC));
            g.drawLine(playPositionInPixel, 0, playPositionInPixel, getHeight());

            // Draw the mouse cursor baloon
            if (mousePositionInPixel >= 0) {
                int mousePositionInSec = (int) ((float) mousePositionInPixel * image.frames / getWidth() / buffer.format().getFrameRate());
                final String mousePosition = String.format("%1$d.%2$02d", mousePositionInSec / 60, mousePositionInSec % 60);
                int mousePositionTextWidth = g.getFontMetrics().stringWidth(mousePosition);
                int mousePositionTextHeight = g.getFontMetrics().getHeight();

                int baloonWidth = mousePositionTextWidth + 4;
                int baloonX = Math.max(0, mousePositionInPixel - baloonWidth);

                g.setColor(Color.WHITE);
                g.fillRect(baloonX, 0, baloonWidth, mousePositionTextHeight + 2);
                g.setColor(Color.BLACK);
                g.drawString(mousePosition, baloonX + 2, mousePositionTextHeight - 2);

            }

        }

    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        setCursor(new java.awt.Cursor(java.awt.Cursor.CROSSHAIR_CURSOR));

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 426, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 150, Short.MAX_VALUE)
        );
    }// </editor-fold>//GEN-END:initComponents
    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables
}
